﻿namespace MetaProperties.Core
{
    using System.Collections.Generic;
    using System.Threading;

    using MetaProperties.Framework;

    /// <summary>
    /// Defines the locking behaviour of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
    /// </summary>
    public enum LockingObservableBehaviour
    {
        /// <summary>
        /// This option guarantees ordering of events in race conditions
        /// but increases the potential for accidental deadlocks as events are raised 
        /// while the lock is still held.
        /// </summary>
        RaiseEventsInsideLock,
        
        /// <summary>
        /// This option releases the lock before raising ValueSet, ValueChanged
        /// and PropertyChanged events.  This gives less potential for accidental deadlocks
        /// but ordering of events is not guaranteed in race conditions.
        /// This makes it less suitable for situations where, for example, you need to detatch from events
        /// on the old value and attach to events on the new value.
        /// </summary>
        RaiseEventsOutsideLockWherePossible,
    }

    /// <summary>
    /// An observable value which is thread safe for both reading and writing.
    /// </summary>
    /// <typeparam name="T">The type of the contained value.</typeparam>
    public class MultiThreadedWriteObservable<T> : TypedObservable<T>, IObservable<T>
    {
        /// <summary>
        /// The reader-writer lock for the contained value.
        /// </summary>
        private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();

        /// <summary>
        /// The contained value.
        /// </summary>
        private T value;

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        public MultiThreadedWriteObservable()
            : this(null, LockingObservableBehaviour.RaiseEventsInsideLock, default(T))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="behaviour">The behaviour.</param>
        public MultiThreadedWriteObservable(LockingObservableBehaviour behaviour)
            : this(null, behaviour, default(T))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="defaultValue">The default value.</param>
        public MultiThreadedWriteObservable(T defaultValue)
            : this(null, LockingObservableBehaviour.RaiseEventsInsideLock, defaultValue)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="behaviour">The behaviour.</param>
        /// <param name="defaultValue">The default value.</param>
        public MultiThreadedWriteObservable(LockingObservableBehaviour behaviour, T defaultValue)
            : this(null, behaviour, defaultValue)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="comparer">The comparer.</param>
        public MultiThreadedWriteObservable(IComparer<T> comparer)
            : this(comparer, LockingObservableBehaviour.RaiseEventsInsideLock, default(T))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="comparer">The comparer.</param>
        /// <param name="behaviour">The behaviour.</param>
        public MultiThreadedWriteObservable(IComparer<T> comparer, LockingObservableBehaviour behaviour)
            : this(comparer, behaviour, default(T))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="comparer">The comparer.</param>
        /// <param name="defaultValue">The default value.</param>
        public MultiThreadedWriteObservable(IComparer<T> comparer, T defaultValue)
            : this(comparer, LockingObservableBehaviour.RaiseEventsInsideLock, defaultValue)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiThreadedWriteObservable&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="comparer">The comparer.</param>
        /// <param name="behaviour">The behaviour.</param>
        /// <param name="defaultValue">The default value.</param>
        public MultiThreadedWriteObservable(IComparer<T> comparer, LockingObservableBehaviour behaviour, T defaultValue)
            : base(comparer)
        {
            this.Behaviour = behaviour;

            this.value = defaultValue;
#if ANCESTRY_SUPPORTED
            this.UpdateValueParent(this.value, UpdateParentOperation.Add);
#endif
        }

        /// <summary>
        /// Gets the reader writer lock.
        /// </summary>
        /// <value>The reader writer lock.</value>
        public ReaderWriterLockSlim ReaderWriterLock
        {
            get { return this.readerWriterLock; }
        }

        /// <summary>
        /// Gets the locking behaviour.
        /// </summary>
        /// <value>The behaviour.</value>
        public LockingObservableBehaviour Behaviour
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets the value of the observable property.
        /// </summary>
        /// <value>The value.</value>
        public T Value
        {
            get
            {
                this.readerWriterLock.EnterReadLock();
                try
                {
                    return this.value;
                }
                finally
                {
                    this.readerWriterLock.ExitReadLock();
                }
            }

            set
            {
                // Goals:
                //  - Events called in correct order even on simultanious cross-thread sets.
                //  - No locks held during event calls
                //  - Support PropertyChanging

                // If we raise events within the lock then ordering is guaranteed
                // but there is deadlock potential from holding the lock.

                // If we raise events outside the lock then ordering is no longer
                // guaranteed.

                // If we keep an event queue to ensure ordering, and raise all events
                // in the queue outside the lock, then the events aren't 
                // necessarily raised on the same thread that we set the value on.
                // Worse, the thread they are raised on may be within some kind
                // of lock higher up the call stack, which gives lots of deadlock potential.

                // If we BeginInvoke event raising to the TA then it is safer.
                // Ordering is guaranteed and we don't hold up the current thread.
                // We also don't make assumptions about the outer locks as we expect
                // all calls to be dispatched.

                // The problem with BeginInvoking is that ValueChanging loses its meaning
                // as you can't get the original value.

                // Updating the parent of the new and old value also is problematic as
                // it triggers further events.  Doing this out of order with setting
                // the new value could have many unexpected consequences, but doing it
                // within the lock would risk deadlocks.

                // We could compromise by losing support for certain features.  For example
                // dispatching could lose support for PropertyChanging and IHost derived values.
                
                // Or raising events in the same thread outside the lock would lose support
                // for strict ordering.
                
                // Or we could raise all events within the locks on the same thread and people
                // would just have to be aware of the consequences... for example you wouldn't be
                // able to invoke to the UI thread and read the value or you'd get deadlock.
                switch (this.Behaviour)
                {
                    case LockingObservableBehaviour.RaiseEventsInsideLock:
                        this.SetValueRaiseEventsInsideLock(value);
                        break;

                    case LockingObservableBehaviour.RaiseEventsOutsideLockWherePossible:
                        this.SetValueRaiseEventsOutsideLock(value);
                        break;
                }
            }
        }

        /// <summary>
        /// Gets the value.
        /// </summary>
        /// <returns>The contained value.</returns>
        protected override object GetValue()
        {
            return this.Value;
        }

        /// <summary>
        /// Sets the value raising events outside the lock.
        /// </summary>
        /// <param name="value">The contained value.</param>
        private void SetValueRaiseEventsOutsideLock(T value)
        {
            bool isChanged = false;
            T oldValue = default(T);

            // Now we lock and double check the value has changed.
            this.readerWriterLock.EnterWriteLock();
            try
            {
                if (!this.AreEqual(this.value, value))
                {
                    isChanged = true;

                    // We have to raise this inside the lock.  If we raise it outside the
                    // lock there is a potential race condition where it either won't be
                    // called enough or will be called too many times.
                    this.OnValueChanging();

#if ANCESTRY_SUPPORTED
        // We have to do this inside the lock to be sure we really want
        // to remove the parent.
                    this.UpdateValueParent(this.value, UpdateParentOperation.Remove);
#endif

                    oldValue = this.value;
                    this.value = value;

#if ANCESTRY_SUPPORTED
        // We have to do this inside the lock to be sure it is called
        // in the correct order under race conditions.
                    this.UpdateValueParent(this.value, UpdateParentOperation.Add);
#endif
                }
            }
            finally
            {
                this.readerWriterLock.ExitWriteLock();
            }

            // Now call the remaining events outside the lock.  This means they may
            // not be called in order, but the sequence numbers can be used to detect
            // when they are out of order if required.
            if (isChanged)
            {
                this.OnValueChanged(
                        oldValue,
                        value);
            }
            else
            {
                this.OnValueSet(
                        value);
            }
        }

        /// <summary>
        /// Sets the value raising events inside the lock.
        /// </summary>
        /// <param name="value">The value.</param>
        private void SetValueRaiseEventsInsideLock(T value)
        {
            // This method is much simpler. We basically just do a normal 
            // value set sequence but inside a lock.  It guarantees ordering
            // but introduces more potential for accidental deadlocks.
            this.readerWriterLock.EnterWriteLock();
            try
            {
                if (!this.AreEqual(this.value, value))
                {
                    this.OnValueChanging();

#if ANCESTRY_SUPPORTED
                    this.UpdateValueParent(this.value, UpdateParentOperation.Remove);
#endif

                    T oldValue = this.value;
                    this.value = value;

#if ANCESTRY_SUPPORTED
                    this.UpdateValueParent(this.value, UpdateParentOperation.Add);
#endif

                    this.OnValueChanged(
                            oldValue,
                            value);
                }
                else
                {
                    this.OnValueSet(
                            value);
                }
            }
            finally
            {
                this.readerWriterLock.ExitWriteLock();
            }
        }
    }
}